package org.marsdonne.commons;

import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.query.QueryUtils;
import org.springframework.lang.Nullable;

import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import javax.transaction.Transactional;
import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

public interface JpaCursorPageable<T extends Identifiable> {

    String getCursorFieldName();

    @Getter
    @NoArgsConstructor
    class Cursor {
        protected Long start;
        protected Long end;

        public Cursor(Long start, Long end) {
            this.start = Optional.ofNullable(start).orElse(0L);
            this.end = Optional.ofNullable(end).orElse(0L);
        }
    }

    @Getter
    class PagedResult<T extends Identifiable> extends Cursor {
        protected List<T> list;

        public PagedResult(long startCursor, long endCursor, List<T> list) {
            super(startCursor, endCursor);
            this.list = list;
        }
    }

    default Class getGenericType() {
        return (Class) ((ParameterizedType) this.getClass().getGenericInterfaces()[0]).getActualTypeArguments()[0];
    }

    Cursor getCursor(@Nullable Specification<T> spec, @Nullable Long after);

    default Cursor getCursor(EntityManager entityManager, @Nullable Specification<T> spec, @Nullable Long after) {
        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
        CriteriaQuery<Cursor> criteriaQuery = criteriaBuilder.createQuery(Cursor.class);
        Root<T> root = criteriaQuery.from(getGenericType());
        root.alias("cursors");

        criteriaQuery.multiselect(
                criteriaBuilder.min(root.get(getCursorFieldName())).alias("start"),
                criteriaBuilder.max(root.get(getCursorFieldName())).alias("end")
        );

        criteriaQuery.where(toPredicates(criteriaBuilder, criteriaQuery, root, after, spec));

        TypedQuery<Cursor> query = entityManager.createQuery(criteriaQuery);

        return query.getSingleResult();
    }

    List<T> pagedRecords(int first, @Nullable Long after, @Nullable Specification<T> spec, Sort sort);

    default List<T> pagedRecords(EntityManager entityManager, int first, @Nullable Long after, @Nullable Specification<T> spec, Sort sort) {
        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
        CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(getGenericType());
        Root<T> root = criteriaQuery.from(getGenericType());
        criteriaQuery.select(root);

        criteriaQuery.where(toPredicates(criteriaBuilder, criteriaQuery, root, after, spec));

        criteriaQuery.orderBy(QueryUtils.toOrders(sort, root, criteriaBuilder));

        TypedQuery<T> query = entityManager.createQuery(criteriaQuery).setMaxResults(first);

        return query.getResultList();
    }

    @Transactional
    default PagedResult<T> paginate(Specification<T> spec, int first, Long after, Sort secondary) {
        Cursor cursors = getCursor(spec, after);

        Sort sort = Sort.by(getCursorFieldName()).descending();
        if (secondary != null && !secondary.isEmpty())
            sort.and(secondary);

        return new PagedResult<>(cursors.start, cursors.end, pagedRecords(first + 1, after, spec, sort));
    }

    default Predicate[] toPredicates(CriteriaBuilder criteriaBuilder, CriteriaQuery criteriaQuery, Root<T> root, @Nullable Long after, @Nullable Specification<T> spec) {
        List<Predicate> predicateList = new ArrayList<>();

        if (after != null)
            predicateList.add(criteriaBuilder.gt(root.get(getCursorFieldName()), after));

        if (spec != null)
            predicateList.add(spec.toPredicate(root, criteriaQuery, criteriaBuilder));


        return predicateList.toArray(new Predicate[predicateList.size()]);
    }
}
